Extension devepment tutorial - Mishiranu/Dashchan-Extensions GitHub Wiki
To be continued...
For the first I want to warn you that it's not easy to make an extension from scratch. But it's not a problem: there are a lot of ready extensions in my repositories. All that you need is download them and change them a little.
Imageboard Software | Extension Implementation | Comment |
---|---|---|
Wakaba | shanachan | Pure Wakaba |
nowere | Almost pure Wakaba | |
cirno | Extended Wakaba | |
Kusaba | chuckdfwk | Almost pure Kusaba or its fork |
chiochan | Extended Kusaba | |
nulleu | Instant-0chan | |
Tinyboard / vichan | haruhichan | With API |
lainchan | With API | |
ponychan | Without API, extended Tinyboard | |
infinite | Infinity fork | |
FoolFuuka | desustorage | 4chan archive |
Every chan extension must extend 4 classes: ChanConfiguration, ChanPerformer, ChanLocator and ChanMarkup.
ChanConfiguration used to determine what chan does: posting, deleting, loading captcha. It also used to store default names, board names and desctiptions.
ChanPerformer used to handle requests: reading threads, sending posts.
ChanLocator used to build URIs and determine the type of URIs.
ChanMarkup used to handle markup buttons and posts text appearance.
These classes will be dynamically loaded by client application. This is how every chan extension works.
You must include DashchanLibrary API library to your project but not compile. You can add this library from jcenter using Gradle:
dependencies {
provided 'com.github.mishiranu:dashchan.library:+'
}
There is an example of Android Manifest file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mishiranu.dashchan.chan.dvach"
android:versionCode="1"
android:versionName="1.8">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23" />
<uses-feature android:name="chan.extension" />
<application android:icon="@drawable/ic_launcher" android:allowBackup="false" android:label="@string/text_app_name">
<meta-data android:name="chan.extension.name" android:value="dvach" />
<meta-data android:name="chan.extension.version" android:value="1" />
<meta-data android:name="chan.extension.icon" android:resource="@drawable/ic_custom_dvach_white" />
<meta-data android:name="chan.extension.source" android:value="https://raw.githubusercontent.com/Mishiranu/Dashchan/master/update/data.json" />
<meta-data android:name="chan.extension.class.configuration" android:value=".DvachChanConfiguration" />
<meta-data android:name="chan.extension.class.performer" android:value=".DvachChanPerformer" />
<meta-data android:name="chan.extension.class.locator" android:value=".DvachChanLocator" />
<meta-data android:name="chan.extension.class.markup" android:value=".DvachChanMarkup" />
<activity android:name="chan.app.UriHandlerActivity" android:label="@string/text_activity_name"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="2ch.hk" />
<data android:host="2ch.pm" />
<data android:host="2ch.cm" />
<data android:host="2ch.re" />
<data android:host="2ch.tf" />
<data android:host="2ch.wf" />
<data android:host="2ch.yt" />
<data android:host="2-ch.so" />
</intent-filter>
</activity>
</application>
</manifest>
Every extension must use chan.extension
feature.
In application block you need to declare the following meta datas:
Name | Value description |
---|---|
chan.extension.name |
A short name of your extension (4-15 lowercase a-z chars) |
chan.extension.version |
API version |
chan.extension.icon |
24dp x 24dp white icon identifier |
chan.extension.source |
JSON file URI containing updating data |
chan.extension.class.configuration |
ChanConfiguration implementation class name |
chan.extension.class.performer |
ChanPerformer implementation class name |
chan.extension.class.locator |
ChanLocator implementation class name |
chan.extension.class.markup |
ChanMarkup implementation class name |
Also your app may contain an activity. It's implemented in DashchanStatic library. You can add this library from jcenter using Gradle:
dependencies {
compile 'com.github.mishiranu:dashchan.static:+'
}
You just need to declare an activity with chan.app.UriHandlerActivity
name and add all possible hosts/schemes to intent-filter
.
You can read a Javadoc that describes every Chan*
class.
You also can use ready extensions as an example.
In constuctor you can request features and apply static configuration.
You also need to implement some configuration getters (obtain*
methods) to configure the client.
The Javadoc describes what methods you must implements. Just do it.
This class tells the client how to handle post markup. You can make text bold, italic, spoiled, etc.
If your extensions supports posting, you also must provide CommentEditor
instance.
This is the hardest part to implement. Every method of this class called by the client to perform requests to the server and handle response.
Read the Javadoc to understand what methods, when and how you must implement.
For example, the iichan.hk
imageboard has 3 domains: iichan.hk
, n.iichan.hk
and on.iichan.hk
. You can define them in constructor:
addChanHost("iichan.hk");
addChanHost("n.iichan.hk");
addChanHost("on.iichan.hk");
In this case user can choose a preferred domain name in applications preferences.
In addition this imageboard can be accessed from iichan.moe
, closed.iichan.hk
, but without posting, so you can tell client to transform these links to user chosen one. These links will be not available in application preferences:
addConvertableChanHost("iichan.moe");
addConvertableChanHost("closed.iichan.hk");
If your imageboard supports HTTPS, you can specify it using setHttpsMode(HttpsMode.CONFIGURABLE);
in constructor.
Let's see what links there are on your imageboard. In case of iichan.hk
:
- 0 board page [1]:
http://iichan.hk/b/
- 0 board page [2]:
http://iichan.hk/b/index.html
- N board page:
http://iichan.hk/b/2.html
- Catalog page:
http://iichan.hk/b/catalogue.html
- Thread page:
http://iichan.hk/b/res/100500.html
- Post in thread [1]:
http://iichan.hk/b/res/100500.html#100500
- Post in thread [2]:
http://iichan.hk/b/res/100500.html#i100500
- Attachment:
http://iichan.hk/b/src/1400000000000.png
You can do it using regular expressions for path. There is a convenient method to do it: isPathMatches
.
- Pattern for board path:
/\w+(/((index|catalogue|\d+)\.html)?)?
- Pattern for thread path:
/\w+/res/\d+\.html
- Pattern for attachment path:
/\w+/src/\d+\.\w+
So this is how you should use them:
private static final Pattern BOARD_PATH = Pattern.compile("/\\w+(?:/(?:(?:index|catalogue|\\d+)\\.html)?)?");
private static final Pattern THREAD_PATH = Pattern.compile("/\\w+/res/\\d+\\.html");
private static final Pattern ATTACHMENT_PATH = Pattern.compile("/\\w+/src/\\d+\\.\\w+");
@Override
public boolean isBoardUri(Uri uri)
{
return isChanHostOrRelative(uri) && isPathMatches(uri, BOARD_PATH);
}
@Override
public boolean isThreadUri(Uri uri)
{
return isChanHostOrRelative(uri) && isPathMatches(uri, THREAD_PATH);
}
@Override
public boolean isAttachmentUri(Uri uri)
{
return isChanHostOrRelative(uri) && isPathMatches(uri, ATTACHMENT_PATH);
}
These methods can be called for all types of URI: board, thread or attachment.
In case of board name you can just extract it from the first path segment:
@Override
public String getBoardName(Uri uri)
{
List<String> segments = uri.getPathSegments();
if (segments.size() > 0) return segments.get(0);
return null;
}
The thread number is harder because it only present in thread URI. You can modify the regex above and extract thread number using it (added group parentheses around \d+ in the end of pattern). Also there is a convenient method getGroupValue
for fast extracting group value.
private static final Pattern THREAD_PATH = Pattern.compile("/\\w+/(?:arch/)?res/(\\d+)\\.html");
@Override
public String getThreadNumber(Uri uri)
{
return uri != null ? getGroupValue(uri.getPath(), THREAD_PATH, 1) : null;
}
The post number contains in thread URI fragment: #100500
or #i100500
. In case of i
I can just return a substring:
@Override
public String getPostNumber(Uri uri)
{
String fragment = uri.getFragment();
if (fragment != null && fragment.startsWith("i")) return fragment.substring(1);
return fragment;
}
ChanLocator also must build URI. It's not too hard, there are some convenient methods such as buildPath
and buildQuery
to do it:
@Override
public Uri createBoardUri(String boardName, int pageNumber)
{
return pageNumber > 0 ? buildPath(boardName, pageNumber + ".html") : buildPath(boardName, "");
}
@Override
public Uri createThreadUri(String boardName, String threadNumber)
{
return buildPath(boardName, "res", threadNumber + ".html");
}
@Override
public Uri createPostUri(String boardName, String threadNumber, String postNumber)
{
return createThreadUri(boardName, threadNumber).buildUpon().fragment(postNumber).build();
}
For example, the 2ch.hk
imageboard can read thread partially, read single post knowing its number and read posts count (which is important for thread watcher). Using request
method you can request these options:
request(OPTION_READ_THREAD_PARTIALLY);
request(OPTION_READ_SINGLE_POST);
request(OPTION_READ_POSTS_COUNT);
You also can specify a default name and bump limit for all boards:
setDefaultName("Аноним");
setBumpLimit(500);
Sometimes client can request some information about your extension. For example, your extension supports reading catalog, posting and sending reports. This configuration is dynamic and can differ on different boards. You can provide it overriding the obtainBoardConfiguration
method:
@Override
public Board obtainBoardConfiguration(String boardName)
{
Board board = new Board();
board.allowCatalog = true;
board.allowPosting = true;
board.allowReporting = true;
return board;
}
In this case client can request from you more detailed information for posting and reporting using obtainPostingConfiguration
and obtainReportingConfiguration
methods. For example:
@Override
public Posting obtainPostingConfiguration(String boardName, boolean replying)
{
Posting posting = new Posting();
posting.allowName = true;
posting.allowTripcode = true;
posting.allowEmail = true;
posting.allowSubject = true;
posting.optionSage = true;
posting.attachmentCount = 1;
posting.attachmentMimeTypes.add("image/*");
posting.attachmentMimeTypes.add("video/webm");
return posting;
}
@Override
public Reporting obtainReportingConfiguration(String boardName)
{
Reporting reporting = new Reporting();
reporting.comment = true;
reporting.multiplePosts = true;
return reporting;
}
Sometimes your configuration may depend on information you retrieved from site. For example, some boards doesn't support names and emails. You can store this data in client storage. For the first, create constants:
private static final String KEY_NAMES_ENABLED = "names_enabled";
private static final String KEY_TRIPCODES_ENABLED = "tripcodes_enabled";
private static final String KEY_EMAILS_ENABLED = "emails_enabled";
Then modify obtainPostingConfiguration
method using get
method to access your storage data:
@Override
public Posting obtainPostingConfiguration(String boardName, boolean replying)
{
Posting posting = new Posting();
posting.allowName = get(boardName, KEY_NAMES_ENABLED, true);
posting.allowTripcode = posting.allowName;
posting.allowEmail = get(boardName, KEY_EMAILS_ENABLED, true);
posting.allowSubject = true;
posting.optionSage = true;
posting.attachmentCount = 1;
posting.attachmentMimeTypes.add("image/*");
posting.attachmentMimeTypes.add("video/webm");
return posting;
}
Using set
method you can edit these parameters where you need:
boolean namesEnabled = ...;
boolean emailsEnabled = ...;
set(boardName, KEY_NAMES_ENABLED, namesEnabled);
set(boardName, KEY_EMAILS_ENABLED, emailsEnabled);
These methods work with boolean
, int
and String
types.
During construction you can tell HTML parser how to handle posts comment.
For example, <b>...</b>
tags are used for bold text, <i>...</i>
for italic, <span class="unkfunc">...</span>
for quotes and <span class="spoiler">...</span>
for spoilers:
addTag("b", TAG_BOLD);
addTag("i", TAG_ITALIC);
addTag("span", "unkfunc", TAG_QUOTE);
addTag("span", "spoiler", TAG_SPOILER);
This class is used when user makes a post. For example, [b]...[/b]
tag is used for bold text, [i]...[/i]
tag is used for italic and '[spoiler]...[/spoiler]' is used for spoilers:
@Override
public CommentEditor obtainCommentEditor(String boardName)
{
CommentEditor commentEditor = new CommentEditor();
commentEditor.addTag(TAG_BOLD, "[b]", "[/b]");
commentEditor.addTag(TAG_ITALIC, "[i]", "[/i]");
commentEditor.addTag(TAG_SPOILER, "[spoiler]", "[/spoiler]");
return commentEditor;
}
The quotes are always formatted with >
in the beginning of the line.
There are some ready comment editors in API. For example, for BBCodes which is named BulletinBoardCodeCommentEditor
. So you can simplify the method above:
@Override
public CommentEditor obtainCommentEditor(String boardName)
{
return new CommentEditor.BulletinBoardCodeCommentEditor();
}
You also must specify what tags your client supported. There is a isTagSupported
method for this purpose you should override. In case of bold, italic and spoilers:
private static final int SUPPORTED_TAGS = TAG_BOLD | TAG_ITALIC | TAG_SPOILER;
@Override
public boolean isTagSupported(String boardName, int tag)
{
return (SUPPORTED_TAGS & tag) == tag;
}
You can use bit operations for TAG_*
constants. The quotation tag is always supported.
This is not necessary, but highly desirable to implement obtainPostLinkThreadPostNumbers
method. This method is used to extract thread number and post number from comment link. ChanLocator
can do it by default, but you can make it faster just implementing this method:
private static final Pattern THREAD_LINK = Pattern.compile("(\\d+).html(?:#(\\d+))?$");
@Override
public Pair<String, String> obtainPostLinkThreadPostNumbers(String uriString)
{
Matcher matcher = THREAD_LINK.matcher(uriString);
if (matcher.find()) return new Pair<>(matcher.group(1), matcher.group(2));
return null;
}
Every Post
item has its own post number. You can specify it using setPostNumber
method.
Every Post
item which is not an original post has a "link" with original post. This link is named parent post number. You can specify it using setParentPostNumber
method. You must specify it for all posts which are not original.
In most cases the number of original post is a thread number. But in some rare cases they are differ. You can specify thread number using setThreadNumber
method.
So, if client requests the thread from your extension, you must provide an array of posts, where the first post is original and the rest posts are not. If client requests a single post, you can specify the posts original post number, so client can understand, whether your provided post is original.