Skip to content

Commit

Permalink
Support Mastodon's custom profile fields
Browse files Browse the repository at this point in the history
  • Loading branch information
grishka committed May 26, 2021
1 parent a236637 commit 1b44bc2
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,9 @@ public static ActivityPubObject parse(JSONObject obj, ParserContext parserContex
case "Relationship":
res=new Relationship();
break;
case "PropertyValue":
res=new PropertyValue();
break;

// Collections
case "Collection":
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/smithereen/activitypub/objects/PropertyValue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package smithereen.activitypub.objects;

import org.json.JSONObject;

import smithereen.Utils;
import smithereen.activitypub.ContextCollector;
import smithereen.activitypub.ParserContext;
import smithereen.jsonld.JLD;

public class PropertyValue extends ActivityPubObject{

public String value;

public PropertyValue(){}

public PropertyValue(String name, String value){
this.name=name;
this.value=value;
}

@Override
public String getType(){
return "PropertyValue";
}

@Override
protected ActivityPubObject parseActivityPubObject(JSONObject obj, ParserContext parserContext) throws Exception{
super.parseActivityPubObject(obj, parserContext);
value=Utils.sanitizeHTML(obj.getString("value"));
return this;
}

@Override
public JSONObject asActivityPubObject(JSONObject obj, ContextCollector contextCollector){
obj=super.asActivityPubObject(obj, contextCollector);
obj.put("value", value);
contextCollector.addAlias("value", JLD.SCHEMA_ORG+"value");
contextCollector.addAlias("PropertyValue", JLD.SCHEMA_ORG+"PropertyValue");
return obj;
}
}
29 changes: 29 additions & 0 deletions src/main/java/smithereen/data/User.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package smithereen.data;

import org.json.JSONArray;
import org.json.JSONObject;

import java.net.URI;
Expand All @@ -10,8 +11,10 @@
import java.security.spec.X509EncodedKeySpec;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;

import smithereen.Config;
import smithereen.activitypub.ContextCollector;
Expand All @@ -20,6 +23,7 @@
import smithereen.activitypub.objects.Actor;
import smithereen.activitypub.objects.Image;
import smithereen.activitypub.objects.LocalImage;
import smithereen.activitypub.objects.PropertyValue;
import smithereen.jsonld.JLD;
import smithereen.storage.MediaCache;
import spark.utils.StringUtils;
Expand Down Expand Up @@ -135,6 +139,18 @@ protected void fillFromResultSet(ResultSet res) throws SQLException{
if(StringUtils.isNotEmpty(fields)){
JSONObject o=new JSONObject(fields);
manuallyApprovesFollowers=o.optBoolean("manuallyApprovesFollowers", false);
if(o.has("custom")){
if(attachment==null)
attachment=new ArrayList<>();
JSONArray custom=o.getJSONArray("custom");
for(int i=0;i<custom.length();i++){
JSONObject fld=custom.getJSONObject(i);
PropertyValue pv=new PropertyValue();
pv.name=fld.getString("n");
pv.value=fld.getString("v");
attachment.add(pv);
}
}
}
}

Expand Down Expand Up @@ -195,6 +211,19 @@ public String serializeProfileFields(){
JSONObject o=new JSONObject();
if(manuallyApprovesFollowers)
o.put("manuallyApprovesFollowers", true);
JSONArray custom=null;
if(attachment!=null){
for(ActivityPubObject att:attachment){
if(att instanceof PropertyValue){
if(custom==null)
custom=new JSONArray();
PropertyValue pv=(PropertyValue) att;
custom.put(Map.of("n", pv.name, "v", pv.value));
}
}
}
if(custom!=null)
o.put("custom", custom);
return o.toString();
}

Expand Down
8 changes: 5 additions & 3 deletions src/main/java/smithereen/jsonld/JLDProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ public class JLDProcessor{
lc.put("toot", JLD.MASTODON);

// schema.org aliases
lc.put("firstName", idAndTypeObject("sc:givenName", "sc:Text"));
lc.put("lastName", idAndTypeObject("sc:familyName", "sc:Text"));
lc.put("middleName", idAndTypeObject("sc:additionalName", "sc:Text"));
lc.put("firstName", "sc:givenName");
lc.put("lastName", "sc:familyName");
lc.put("middleName", "sc:additionalName");
lc.put("gender", idAndTypeObject("sc:gender", "sc:GenderType"));
lc.put("birthDate", idAndTypeObject("sc:birthDate", "sc:Date"));
lc.put("value", "sc:value");
lc.put("PropertyValue", "sc:PropertyValue");

// ActivityStreams aliases
lc.put("sensitive", "as:sensitive");
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/smithereen/routes/ProfileRoutes.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.URI;
import java.net.URLEncoder;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -15,6 +16,7 @@
import smithereen.Config;
import smithereen.Utils;
import smithereen.activitypub.ActivityPubWorker;
import smithereen.activitypub.objects.PropertyValue;
import smithereen.data.Account;
import smithereen.data.ForeignUser;
import smithereen.data.FriendRequest;
Expand Down Expand Up @@ -44,6 +46,7 @@ public static Object profile(Request req, Response resp) throws SQLException{
@Nullable Account self=info!=null ? info.account : null;
String username=req.params(":username");
User user=UserStorage.getByUsername(username);
Lang l=lang(req);
if(user!=null){
int[] postCount={0};
int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0);
Expand All @@ -67,6 +70,15 @@ public static Object profile(Request req, Response resp) throws SQLException{
}
}

ArrayList<PropertyValue> profileFields=new ArrayList<>();
if(user.birthDate!=null)
profileFields.add(new PropertyValue(l.get("birth_date"), l.formatDay(user.birthDate.toLocalDate())));
if(StringUtils.isNotEmpty(user.summary))
profileFields.add(new PropertyValue(l.get("profile_about"), user.summary));
if(user.attachment!=null)
user.attachment.stream().filter(o->o instanceof PropertyValue).forEach(o->profileFields.add((PropertyValue) o));
model.with("profileFields", profileFields);

if(info!=null && self!=null){
model.with("draftAttachments", info.postDraftAttachments);
}
Expand Down Expand Up @@ -112,7 +124,6 @@ public static Object profile(Request req, Response resp) throws SQLException{
meta.put("og:first_name", user.firstName);
if(StringUtils.isNotEmpty(user.lastName))
meta.put("og:last_name", user.lastName);
Lang l=Utils.lang(req);
String descr=l.plural("X_friends", friendCount[0])+", "+l.plural("X_posts", postCount[0]);
if(StringUtils.isNotEmpty(user.summary))
descr+="\n"+user.summary;
Expand Down
63 changes: 33 additions & 30 deletions src/main/java/smithereen/storage/UserStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -489,43 +489,46 @@ public static void updateProfilePicture(User user, String serializedPic) throws

public static synchronized int putOrUpdateForeignUser(ForeignUser user) throws SQLException{
Connection conn=DatabaseConnectionManager.getConnection();
PreparedStatement stmt=conn.prepareStatement("SELECT `id` FROM `users` WHERE `ap_id`=?");
stmt.setString(1, Objects.toString(user.activityPubID, null));
PreparedStatement stmt=new SQLQueryBuilder(conn)
.selectFrom("users")
.columns("id")
.where("ap_id=?", Objects.toString(user.activityPubID))
.createStatement();
int existingUserID=0;
try(ResultSet res=stmt.executeQuery()){
if(res.first())
existingUserID=res.getInt(1);
}
SQLQueryBuilder bldr=new SQLQueryBuilder(conn);
if(existingUserID!=0){
stmt=conn.prepareStatement("UPDATE `users` SET `fname`=?,`lname`=?,`bdate`=?,`username`=?,`domain`=?,`public_key`=?,`ap_url`=?,`ap_inbox`=?,`ap_outbox`=?,`ap_shared_inbox`=?,`ap_id`=?,`ap_followers`=?,`ap_following`=?," +
"`about`=?,`gender`=?,`avatar`=?,`profile_fields`=?,`flags`=?,middle_name=?,maiden_name=?,ap_wall=?,`last_updated`=CURRENT_TIMESTAMP() WHERE `id`=?");
stmt.setInt(22, existingUserID);
bldr.update("users").where("id=?", existingUserID);
}else{
stmt=conn.prepareStatement("INSERT INTO `users` (`fname`,`lname`,`bdate`,`username`,`domain`,`public_key`,`ap_url`,`ap_inbox`,`ap_outbox`,`ap_shared_inbox`,`ap_id`,`ap_followers`,`ap_following`,`about`,`gender`,`avatar`,`profile_fields`,`flags`,middle_name,maiden_name,ap_wall,`last_updated`)" +
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP())", PreparedStatement.RETURN_GENERATED_KEYS);
}

stmt.setString(1, user.firstName);
stmt.setString(2, user.lastName);
stmt.setDate(3, user.birthDate);
stmt.setString(4, user.username);
stmt.setString(5, user.domain);
stmt.setBytes(6, user.publicKey.getEncoded());
stmt.setString(7, Objects.toString(user.url, null));
stmt.setString(8, Objects.toString(user.inbox, null));
stmt.setString(9, Objects.toString(user.outbox, null));
stmt.setString(10, Objects.toString(user.sharedInbox, null));
stmt.setString(11, Objects.toString(user.activityPubID, null));
stmt.setString(12, Objects.toString(user.followers, null));
stmt.setString(13, Objects.toString(user.following, null));
stmt.setString(14, user.summary);
stmt.setInt(15, user.gender==null ? 0 : user.gender.ordinal());
stmt.setString(16, user.icon!=null ? user.icon.get(0).asActivityPubObject(new JSONObject(), new ContextCollector()).toString() : null);
stmt.setString(17, user.serializeProfileFields());
stmt.setLong(18, user.flags);
stmt.setString(19, user.middleName);
stmt.setString(20, user.maidenName);
stmt.setString(21, Objects.toString(user.getWallURL(), null));
bldr.insertInto("users");
}

bldr.valueExpr("last_updated", "CURRENT_TIMESTAMP()")
.value("fname", user.firstName)
.value("lname", user.lastName)
.value("bdate", user.birthDate)
.value("username", user.username)
.value("domain", user.domain)
.value("public_key", user.publicKey.getEncoded())
.value("ap_url", Objects.toString(user.url, null))
.value("ap_inbox", Objects.toString(user.inbox, null))
.value("ap_outbox", Objects.toString(user.outbox, null))
.value("ap_shared_inbox", Objects.toString(user.sharedInbox, null))
.value("ap_id", user.activityPubID.toString())
.value("ap_followers", Objects.toString(user.followers, null))
.value("ap_following", Objects.toString(user.following, null))
.value("about", user.summary)
.value("gender", user.gender)
.value("avatar", user.icon!=null ? user.icon.get(0).asActivityPubObject(new JSONObject(), new ContextCollector()).toString() : null)
.value("profile_fields", user.serializeProfileFields())
.value("flags", user.flags)
.value("middle_name", user.middleName)
.value("maiden_name", user.maidenName)
.value("ap_wall", Objects.toString(user.getWallURL(), null));
stmt=existingUserID==0 ? bldr.createStatement() : bldr.createStatement(PreparedStatement.RETURN_GENERATED_KEYS);

stmt.executeUpdate();
if(existingUserID==0){
Expand Down
8 changes: 6 additions & 2 deletions src/main/resources/templates/desktop/profile.twig
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,12 @@
<h2>{{user.completeName}}</h2>
<div class="profileFields">
<table class="profileBlock" width="100%">
{%if user.birthDate is not null%}<tr><td class="label">{{L("birth_date")}}:</td><td>{{ LD(user.birthDate) }}</td></tr>{%endif%}
{%if user.summary is not null%}<tr><td class="label">{{L('profile_about')}}:</td><td>{{ user.summary | postprocessHTML }}</td></tr>{%endif%}
{% for fld in profileFields %}
<tr>
<td class="label">{{ fld.name }}:</td>
<td>{{ fld.value | postprocessHTML }}</td>
</tr>
{% endfor %}
</table>
</div>
<table width="100%" class="profileBlock">
Expand Down
12 changes: 4 additions & 8 deletions src/main/resources/templates/mobile/profile.twig
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,10 @@
</div>

<div class="card profileFields">
{%if user.birthDate is not null%}
<div class="profileFieldName">{{L("birth_date")}}</div>
<div class="profileFieldValue">{{ LD(user.birthDate) }}</div>
{%endif%}
{%if user.summary is not null%}
<div class="profileFieldName">{{L('profile_about')}}</div>
<div class="profileFieldValue">{{ user.summary | postprocessHTML }}</div>
{%endif%}
{% for fld in profileFields %}
<div class="profileFieldName">{{ fld.name }}</div>
<div class="profileFieldValue">{{ fld.value | postprocessHTML }}</div>
{% endfor %}
</div>

{% if currentUser is not null and not isSelfBlocked %}
Expand Down
4 changes: 4 additions & 0 deletions src/main/web/desktop.scss
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ h2{
line-height: 140%;
padding: 10px 6px 15px;
td{
padding-top: 3px;
vertical-align: top;
p:first-child{
margin-top: 0;
Expand All @@ -477,6 +478,9 @@ h2{
margin-bottom: 0;
}
}
tr:first-child td{
padding-top: 0;
}
.label{
color: $auxiliaryGrey;
width: 30%;
Expand Down
6 changes: 6 additions & 0 deletions src/main/web/mobile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,12 @@ select{
}
.profileFieldValue{
padding-bottom: 16px;
p:first-child{
margin-top: 0;
}
p:last-child{
margin-bottom: 0;
}
}
}

Expand Down

0 comments on commit 1b44bc2

Please sign in to comment.