git.gnu.io has moved to IP address 209.51.188.249 -- please double check where you are logging in.

Commit b4da5f37 authored by Evan Prodromou's avatar Evan Prodromou

Merge branch 'master' into 1.0.x

Conflicts:
	plugins/Blacklist/BlacklistPlugin.php
parents de7ad991 12921d6b
...@@ -92,8 +92,6 @@ class ApiSearchAtomAction extends ApiPrivateAuthAction ...@@ -92,8 +92,6 @@ class ApiSearchAtomAction extends ApiPrivateAuthAction
*/ */
function prepare($args) function prepare($args)
{ {
common_debug("in apisearchatom prepare()");
parent::prepare($args); parent::prepare($args);
$this->query = $this->trimmed('q'); $this->query = $this->trimmed('q');
......
...@@ -63,8 +63,6 @@ class ApiSearchJSONAction extends ApiPrivateAuthAction ...@@ -63,8 +63,6 @@ class ApiSearchJSONAction extends ApiPrivateAuthAction
*/ */
function prepare($args) function prepare($args)
{ {
common_debug("apisearchjson prepare()");
parent::prepare($args); parent::prepare($args);
$this->query = $this->trimmed('q'); $this->query = $this->trimmed('q');
......
...@@ -159,6 +159,11 @@ class PublicAction extends Action ...@@ -159,6 +159,11 @@ class PublicAction extends Action
$this->element('link', array('rel' => 'EditURI', $this->element('link', array('rel' => 'EditURI',
'type' => 'application/rsd+xml', 'type' => 'application/rsd+xml',
'href' => $rsd)); 'href' => $rsd));
if ($this->page != 1) {
$this->element('link', array('rel' => 'canonical',
'href' => common_local_url('public')));
}
} }
/** /**
......
...@@ -288,10 +288,11 @@ class RecoverpasswordAction extends Action ...@@ -288,10 +288,11 @@ class RecoverpasswordAction extends Action
'have been sent to the email address registered to your ' . 'have been sent to the email address registered to your ' .
'account.'); 'account.');
$this->success = true; $this->success = true;
$this->showPage();
} catch (Exception $e) { } catch (Exception $e) {
$this->success = false; $this->success = false;
$this->msg = $e->getMessage();
} }
$this->showPage();
} }
function resetPassword() function resetPassword()
......
...@@ -233,4 +233,12 @@ class ShowgroupAction extends GroupAction ...@@ -233,4 +233,12 @@ class ShowgroupAction extends GroupAction
$this->raw(common_markup_to_html($m)); $this->raw(common_markup_to_html($m));
$this->elementEnd('div'); $this->elementEnd('div');
} }
function extraHead()
{
if ($this->page != 1) {
$this->element('link', array('rel' => 'canonical',
'href' => $this->group->homeUrl()));
}
}
} }
...@@ -212,6 +212,11 @@ class ShowstreamAction extends ProfileAction ...@@ -212,6 +212,11 @@ class ShowstreamAction extends ProfileAction
$this->element('link', array('rel' => 'EditURI', $this->element('link', array('rel' => 'EditURI',
'type' => 'application/rsd+xml', 'type' => 'application/rsd+xml',
'href' => $rsd)); 'href' => $rsd));
if ($this->page != 1) {
$this->element('link', array('rel' => 'canonical',
'href' => $this->profile->profileurl));
}
} }
function showEmptyListMessage() function showEmptyListMessage()
......
...@@ -29,8 +29,6 @@ class UserrssAction extends Rss10Action ...@@ -29,8 +29,6 @@ class UserrssAction extends Rss10Action
function prepare($args) function prepare($args)
{ {
common_debug("UserrssAction");
parent::prepare($args); parent::prepare($args);
$nickname = $this->trimmed('nickname'); $nickname = $this->trimmed('nickname');
$this->user = User::staticGet('nickname', $nickname); $this->user = User::staticGet('nickname', $nickname);
......
...@@ -1086,7 +1086,7 @@ class User extends Managed_DataObject ...@@ -1086,7 +1086,7 @@ class User extends Managed_DataObject
if (!$user) { if (!$user) {
// TRANS: Information on password recovery form if no known username or e-mail address was specified. // TRANS: Information on password recovery form if no known username or e-mail address was specified.
throw new ClientError(_('No user with that email address or username.')); throw new ClientException(_('No user with that email address or username.'));
return; return;
} }
......
...@@ -302,7 +302,7 @@ class Action extends HTMLOutputter // lawsuit ...@@ -302,7 +302,7 @@ class Action extends HTMLOutputter // lawsuit
$this->script('jquery.form.min.js'); $this->script('jquery.form.min.js');
$this->script('jquery-ui.min.js'); $this->script('jquery-ui.min.js');
$this->script('jquery.cookie.min.js'); $this->script('jquery.cookie.min.js');
$this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.min.js').'"); }'); $this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.min.js', StatusNet::isHTTPS()).'"); }');
$this->script('jquery.joverlay.min.js'); $this->script('jquery.joverlay.min.js');
$this->script('jquery.infieldlabel.min.js'); $this->script('jquery.infieldlabel.min.js');
} else { } else {
...@@ -310,7 +310,7 @@ class Action extends HTMLOutputter // lawsuit ...@@ -310,7 +310,7 @@ class Action extends HTMLOutputter // lawsuit
$this->script('jquery.form.js'); $this->script('jquery.form.js');
$this->script('jquery-ui.min.js'); $this->script('jquery-ui.min.js');
$this->script('jquery.cookie.js'); $this->script('jquery.cookie.js');
$this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.js').'"); }'); $this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.js', StatusNet::isHTTPS()).'"); }');
$this->script('jquery.joverlay.js'); $this->script('jquery.joverlay.js');
$this->script('jquery.infieldlabel.js'); $this->script('jquery.infieldlabel.js');
} }
......
...@@ -389,9 +389,28 @@ class Activity ...@@ -389,9 +389,28 @@ class Activity
$activity['geopoint'] = array( $activity['geopoint'] = array(
'type' => 'Point', 'type' => 'Point',
'coordinates' => array($loc->lat, $loc->lon) 'coordinates' => array($loc->lat, $loc->lon),
'deprecated' => true,
); );
$activity['location'] = array(
'objectType' => 'place',
'position' => sprintf("%+02.5F%+03.5F/", $loc->lat, $loc->lon),
'lat' => $loc->lat,
'lon' => $loc->lon
);
$name = $loc->getName();
if ($name) {
$activity['location']['displayName'] = $name;
}
$url = $loc->getURL();
if ($url) {
$activity['location']['url'] = $url;
}
} }
$activity['to'] = $this->context->getToArray(); $activity['to'] = $this->context->getToArray();
...@@ -487,15 +506,20 @@ class Activity ...@@ -487,15 +506,20 @@ class Activity
/* Purely extensions hereafter */ /* Purely extensions hereafter */
$tags = array(); if ($activity['verb'] == 'post') {
$tags = array();
// Use an Activity Object for term? Which object? Note? foreach ($this->categories as $cat) {
foreach ($this->categories as $cat) { if (mb_strlen($cat->term) > 0) {
$tags[] = $cat->term; // Couldn't figure out which object type to use, so...
$tags[] = array('objectType' => 'http://activityschema.org/object/hashtag',
'displayName' => $cat->term);
}
}
if (count($tags) > 0) {
$activity['object']['tags'] = $tags;
}
} }
$activity['tags'] = $tags;
// XXX: a bit of a hack... Since JSON isn't namespaced we probably // XXX: a bit of a hack... Since JSON isn't namespaced we probably
// shouldn't be using 'statusnet:notice_info', but this will work // shouldn't be using 'statusnet:notice_info', but this will work
// for the moment. // for the moment.
......
...@@ -692,9 +692,6 @@ class ActivityObject ...@@ -692,9 +692,6 @@ class ActivityObject
// id // id
$object['id'] = $this->id; $object['id'] = $this->id;
//
// XXX: Should we use URL here? or a crazy tag URI?
$object['id'] = $this->id;
if ($this->type == ActivityObject::PERSON if ($this->type == ActivityObject::PERSON
|| $this->type == ActivityObject::GROUP) { || $this->type == ActivityObject::GROUP) {
...@@ -737,14 +734,17 @@ class ActivityObject ...@@ -737,14 +734,17 @@ class ActivityObject
// We can probably use the whole schema URL here but probably the // We can probably use the whole schema URL here but probably the
// relative simple name is easier to parse // relative simple name is easier to parse
// @fixme this breaks extension URIs // @fixme this breaks extension URIs
$object['type'] = substr($this->type, strrpos($this->type, '/') + 1); $object['objectType'] = substr($this->type, strrpos($this->type, '/') + 1);
// published (probably don't need. Might be useful for repeats.)
// summary // summary
$object['summary'] = $this->summary; $object['summary'] = $this->summary;
// udpated (probably don't need this) // summary
$object['content'] = $this->content;
// published (probably don't need. Might be useful for repeats.)
// updated (probably don't need this)
// TODO: upstreamDuplicates // TODO: upstreamDuplicates
......
...@@ -168,7 +168,7 @@ class ActivityStreamJSONDocument extends JSONActivityCollection ...@@ -168,7 +168,7 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
$this->doc['generator'] = 'StatusNet ' . STATUSNET_VERSION; // extension $this->doc['generator'] = 'StatusNet ' . STATUSNET_VERSION; // extension
$this->doc['title'] = $this->title; $this->doc['title'] = $this->title;
$this->doc['url'] = $this->url; $this->doc['url'] = $this->url;
$this->doc['count'] = $this->count; $this->doc['totalItems'] = $this->count;
$this->doc['items'] = $this->items; $this->doc['items'] = $this->items;
$this->doc['links'] = $this->links; // extension $this->doc['links'] = $this->links; // extension
return json_encode(array_filter($this->doc)); // filter out empty elements return json_encode(array_filter($this->doc)); // filter out empty elements
......
...@@ -66,7 +66,7 @@ $default = ...@@ -66,7 +66,7 @@ $default =
'minify' => true, // true to use the minified versions of JS files; false to use orig files. Can aid during development 'minify' => true, // true to use the minified versions of JS files; false to use orig files. Can aid during development
), ),
'db' => 'db' =>
array('database' => 'YOU HAVE TO SET THIS IN config.php', array('database' => null, // must be set
'schema_location' => INSTALLDIR . '/classes', 'schema_location' => INSTALLDIR . '/classes',
'class_location' => INSTALLDIR . '/classes', 'class_location' => INSTALLDIR . '/classes',
'require_prefix' => 'classes/', 'require_prefix' => 'classes/',
......
...@@ -160,6 +160,15 @@ function PEAR_ErrorToPEAR_Exception($err) ...@@ -160,6 +160,15 @@ function PEAR_ErrorToPEAR_Exception($err)
common_log(LOG_ERR, "PEAR Error: $msg ($userInfo)"); common_log(LOG_ERR, "PEAR Error: $msg ($userInfo)");
// HACK: queue handlers get kicked by the long-query killer, and
// keep the same broken connection. We die here to get a new
// process started.
if (php_sapi_name() == 'cli' && preg_match('/nativecode=2006/', $userInfo)) {
common_log(LOG_ERR, "Lost DB connection; dying.");
exit(100);
}
if ($err->getCode()) { if ($err->getCode()) {
throw new PEAR_Exception($msg, $err, $err->getCode()); throw new PEAR_Exception($msg, $err, $err->getCode());
} else { } else {
......
...@@ -720,4 +720,17 @@ class NoticeListItem extends Widget ...@@ -720,4 +720,17 @@ class NoticeListItem extends Widget
Event::handle('EndCloseNoticeListItemElement', array($this)); Event::handle('EndCloseNoticeListItemElement', array($this));
} }
} }
/**
* Get the notice in question
*
* For hooks, etc., this may be useful
*
* @return Notice The notice we're showing
*/
function getNotice()
{
return $this->notice;
}
} }
...@@ -159,7 +159,12 @@ class Plugin ...@@ -159,7 +159,12 @@ class Plugin
} }
if (empty($path)) { if (empty($path)) {
$path = common_config('site', 'path') . '/plugins/'; // XXX: extra stat().
if (@file_exists(INSTALLDIR.'/local/plugins/'.$plugin.'/'.$relative)) {
$path = common_config('site', 'path') . '/local/plugins/';
} else {
$path = common_config('site', 'path') . '/plugins/';
}
} }
if ($path[strlen($path)-1] != '/') { if ($path[strlen($path)-1] != '/') {
......
...@@ -127,6 +127,7 @@ abstract class ProfileBlock extends Widget ...@@ -127,6 +127,7 @@ abstract class ProfileBlock extends Widget
if (!empty($homepage)) { if (!empty($homepage)) {
$this->out->element('a', $this->out->element('a',
array('href' => $homepage, array('href' => $homepage,
'rel' => 'me',
'class' => 'profile_block_homepage'), 'class' => 'profile_block_homepage'),
$homepage); $homepage);
} }
......
...@@ -362,6 +362,7 @@ class StatusNet ...@@ -362,6 +362,7 @@ class StatusNet
if (@file_exists($_config_file)) { if (@file_exists($_config_file)) {
// Ignore 0-byte config files // Ignore 0-byte config files
if (filesize($_config_file) > 0) { if (filesize($_config_file) > 0) {
common_log(LOG_INFO, "Including config file: " . $_config_file);
include($_config_file); include($_config_file);
self::$have_config = true; self::$have_config = true;
} }
...@@ -383,6 +384,7 @@ class StatusNet ...@@ -383,6 +384,7 @@ class StatusNet
$config['cache']['base'] = $config['memcached']['base']; $config['cache']['base'] = $config['memcached']['base'];
} }
} }
if (array_key_exists('xmpp', $config)) { if (array_key_exists('xmpp', $config)) {
if ($config['xmpp']['enabled']) { if ($config['xmpp']['enabled']) {
addPlugin('xmpp', array( addPlugin('xmpp', array(
...@@ -398,6 +400,12 @@ class StatusNet ...@@ -398,6 +400,12 @@ class StatusNet
)); ));
} }
} }
// Check for database server; must exist!
if (empty($config['db']['database'])) {
throw new ServerException("No database server for this site.");
}
} }
/** /**
......
...@@ -80,6 +80,19 @@ class Widget ...@@ -80,6 +80,19 @@ class Widget
{ {
} }
/**
* Get HTMLOutputter
*
* Return the HTMLOutputter for the widget.
*
* @return HTMLOutputter the output helper
*/
function getOut()
{
return $this->out;
}
/** /**
* Delegate output methods to the outputter attribute. * Delegate output methods to the outputter attribute.
* *
......
...@@ -505,14 +505,16 @@ class BlacklistPlugin extends Plugin ...@@ -505,14 +505,16 @@ class BlacklistPlugin extends Plugin
} }
} }
$nickname = strtolower($actor->poco->preferredUsername); if (!empty($actor->poco)) {
$nickname = strtolower($actor->poco->preferredUsername);
if (!empty($nickname)) {
if (!$this->_checkNickname($nickname)) { if (!empty($nickname)) {
// TRANS: Exception thrown trying to post a notice while having a blocked nickname. %s is the blocked nickname. if (!$this->_checkNickname($nickname)) {
$msg = sprintf(_m("Notices from nickname \"%s\" are disallowed."), // TRANS: Exception thrown trying to post a notice while having a blocked nickname. %s is the blocked nickname.
$nickname); $msg = sprintf(_m("Notices from nickname \"%s\" disallowed."),
throw new ClientException($msg); $nickname);
throw new ClientException($msg);
}
} }
} }
......
...@@ -572,4 +572,20 @@ class BookmarkPlugin extends MicroAppPlugin ...@@ -572,4 +572,20 @@ class BookmarkPlugin extends MicroAppPlugin
$notice->update($original); $notice->update($original);
} }
} }
public function activityObjectOutputJson(ActivityObject $obj, array &$out)
{
assert($obj->type == ActivityObject::BOOKMARK);
$bm = Bookmark::staticGet('uri', $obj->id);
if (empty($bm)) {
throw new ServerException("Unknown bookmark: " . $obj->id);
}
$out['displayName'] = $bm->title;
$out['targetUrl'] = $bm->url;
return true;
}
} }
...@@ -176,7 +176,8 @@ class Facebookclient ...@@ -176,7 +176,8 @@ class Facebookclient
// If it's not a reply, or if the user WANTS to send @-replies, // If it's not a reply, or if the user WANTS to send @-replies,
// then, yeah, it can go to Facebook. // then, yeah, it can go to Facebook.
if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $this->notice->content) ||
if (empty($this->notice->reply_to) ||
($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) { ($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
return true; return true;
} }
......
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$helptext = <<<END_OF_HELP
gcfeeds.php [options]
Clean up feeds that no longer have subscribers.
END_OF_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
$feedsub = new FeedSub();
while ($feedsub->fetch()) {
print $feedsub->uri . "(" . $feedsub->sub_state . ")";
$result = $feedsub->garbageCollect();
if ($result) {
print " INACTIVE\n";
} else {
print " ACTIVE\n";
}
}
...@@ -223,6 +223,7 @@ class RegisterThrottlePlugin extends Plugin ...@@ -223,6 +223,7 @@ class RegisterThrottlePlugin extends Plugin
private function _getIpAddress() private function _getIpAddress()
{ {
$keys = array('HTTP_X_FORWARDED_FOR', $keys = array('HTTP_X_FORWARDED_FOR',
'HTTP_X_CLIENT',
'CLIENT-IP', 'CLIENT-IP',
'REMOTE_ADDR'); 'REMOTE_ADDR');
......
...@@ -65,6 +65,9 @@ class SphinxSearch extends SearchEngine ...@@ -65,6 +65,9 @@ class SphinxSearch extends SearchEngine
function query($q) function query($q)
{ {
$result = $this->sphinx->query($q, $this->remote_table()); $result = $this->sphinx->query($q, $this->remote_table());
if ($result === false) {
throw new ServerException($this->sphinx->getLastError());
}
if (!isset($result['matches'])) return false; if (!isset($result['matches'])) return false;
$id_set = join(', ', array_keys($result['matches'])); $id_set = join(', ', array_keys($result['matches']));
$this->target->whereAdd("id in ($id_set)"); $this->target->whereAdd("id in ($id_set)");
......
...@@ -157,6 +157,10 @@ class SubMirrorPlugin extends Plugin ...@@ -157,6 +157,10 @@ class SubMirrorPlugin extends Plugin
*/ */
function onOstatus_profileSubscriberCount($oprofile, &$count) function onOstatus_profileSubscriberCount($oprofile, &$count)
{ {
if (empty($oprofile) || !($oprofile instanceof Ostatus_profile)) {
return true;
}
if ($oprofile->profile_id) { if ($oprofile->profile_id) {
$mirror = new SubMirror(); $mirror = new SubMirror();
$mirror->subscribed = $oprofile->profile_id; $mirror->subscribed = $oprofile->profile_id;
...@@ -166,6 +170,7 @@ class SubMirrorPlugin extends Plugin ...@@ -166,6 +170,7 @@ class SubMirrorPlugin extends Plugin
} }
} }
} }
return true; return true;
} }
......
...@@ -157,7 +157,7 @@ class SubMirror extends Memcached_DataObject ...@@ -157,7 +157,7 @@ class SubMirror extends Memcached_DataObject
{ {
$profile = Profile::staticGet('id', $this->subscriber); $profile = Profile::staticGet('id', $this->subscriber);
if (!$profile) { if (!$profile) {
common_log(LOG_ERROR, "SubMirror plugin skipping auto-repeat of notice $notice->id for missing user $profile->id"); common_log(LOG_ERR, "SubMirror plugin skipping auto-repeat of notice $notice->id for missing user $profile->id");
return false; return false;
} }
......
...@@ -339,7 +339,7 @@ class TwitterBridgePlugin extends Plugin ...@@ -339,7 +339,7 @@ class TwitterBridgePlugin extends Plugin
$versions[] = array( $versions[] = array(
'name' => 'TwitterBridge', 'name' => 'TwitterBridge',
'version' => self::VERSION, 'version' => self::VERSION,
'author' => 'Zach Copley, Julien C', 'author' => 'Zach Copley, Julien C, Jean Baptiste Favre',
'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge', 'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge',
// TRANS: Plugin description. // TRANS: Plugin description.
'rawdescription' => _m('The Twitter "bridge" plugin allows integration ' . 'rawdescription' => _m('The Twitter "bridge" plugin allows integration ' .
......
...@@ -116,13 +116,13 @@ function is_twitter_bound($notice, $flink) { ...@@ -116,13 +116,13 @@ function is_twitter_bound($notice, $flink) {
} }
// Check to see if notice should go to Twitter // Check to see if notice should go to Twitter
if (!empty($flink) && ($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) { if (!empty($flink) && (($flink->noticesync & FOREIGN_NOTICE_SEND) == FOREIGN_NOTICE_SEND)) {
// If it's not a Twitter-style reply, or if the user WANTS to send replies, // If it's not a Twitter-style reply, or if the user WANTS to send replies,
// or if it's in reply to a twitter notice // or if it's in reply to a twitter notice
if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) || if ( (($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) == FOREIGN_NOTICE_SEND_REPLY) ||
($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY == FOREIGN_NOTICE_SEND_REPLY) || (is_twitter_notice($notice->reply_to) || is_twitter_notice($notice->repeat_of)) ||
is_twitter_notice($notice->reply_to)) { (empty($notice->reply_to) && !preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content)) ){
return true; return true;
} }
} }
......
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2012 StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
$shortoptions = 'i:n:g:G:';
$longoptions = array('id=', 'nickname=', 'group=', 'group-id=');
$helptext = <<<END_OF_HELP
addusertogroup.php [options]
Adds a local user to a local group.
-i --id ID of user to add
-n --nickname nickname of the user to add
-g --group nickname or alias of group
-G --group-id ID of group
END_OF_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
try {
$user = getUser();
$lgroup = null;
if (have_option('G', 'group-id')) {
$gid = get_option_value('G', 'group-id');
$lgroup = Local_group::staticGet('group_id', $gid);
} else if (have_option('g', 'group')) {
$gnick = get_option_value('g', 'group');
$lgroup = Local_group::staticGet('nickname', $gnick);
}
if (empty($lgroup)) {
throw new Exception("No such local group: $gnick");
}
$group = User_group::staticGet('id', $lgroup->group_id);