CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-github-binarywang--weixin-java-miniapp

Comprehensive Java SDK for WeChat MiniApp development with complete platform integration

Pending
Overview
Eval results
Files

media-content.mddocs/

Media & Content

Media file management, content security, and content moderation services for images, videos, audio files, and other media types in WeChat MiniApp applications.

Capabilities

Media Service Interface

Core media operations for uploading, managing, and processing media files.

public interface WxMaMediaService {
    // Media Upload
    WxMediaUploadResult uploadMedia(String type, File media) throws WxErrorException;
    WxMediaUploadResult uploadMedia(String type, InputStream inputStream, String fileName) throws WxErrorException;
    
    // Media Download  
    File downloadMedia(String mediaId, File file) throws WxErrorException;
    InputStream downloadMedia(String mediaId) throws WxErrorException;
    
    // Temporary Media (3 days retention)
    WxMediaUploadResult uploadTempMedia(String type, File media) throws WxErrorException;
    WxMediaUploadResult uploadTempMedia(String type, InputStream inputStream, String fileName) throws WxErrorException;
    
    // Image Upload (permanent)
    WxMediaUploadResult uploadImg(File media) throws WxErrorException;
    WxMediaUploadResult uploadImg(InputStream inputStream, String fileName) throws WxErrorException;
}

Security Service Interface

Content security and moderation operations for text, images, and other media content.

public interface WxMaSecurityService {
    // Text Security Check
    boolean checkText(String content) throws WxErrorException;
    boolean checkText(String content, Integer scene, Integer version) throws WxErrorException;
    
    // Image Security Check  
    boolean checkImage(File imageFile) throws WxErrorException;
    boolean checkImage(String mediaId) throws WxErrorException;
    boolean checkImage(InputStream inputStream, String fileName) throws WxErrorException;
    
    // Audio Security Check
    boolean checkAudio(File audioFile) throws WxErrorException;
    boolean checkAudio(String mediaId) throws WxErrorException;
    
    // User Risk Assessment
    WxMaUserRiskRank getUserRiskRank(String openid, Integer scene) throws WxErrorException;
}

Media Upload Models

Data models for media upload operations and responses.

public class WxMediaUploadResult implements Serializable {
    private String type;                // Media type (image, voice, video, thumb)
    private String mediaId;             // Media ID for subsequent operations
    private Long createdAt;             // Upload timestamp
    private String url;                 // Media URL (for permanent images)
    
    // Getters and setters
    public String getType();
    public void setType(String type);
    public String getMediaId();
    public void setMediaId(String mediaId);
    public Long getCreatedAt();
    public void setCreatedAt(Long createdAt);
    public String getUrl();
    public void setUrl(String url);
    
    // Utility methods
    public Date getCreatedAtAsDate();
    public static WxMediaUploadResult fromJson(String json);
    public String toJson();
}

Security Check Models

Data models for content security and user risk assessment.

public class WxMaSecurityCheckResult implements Serializable {
    private Integer errCode;            // Error code (0: pass, 87014: risky content)
    private String errMsg;              // Error message
    private String traceId;             // Trace ID for tracking
    private SecurityResult result;      // Detailed security check result
    
    public static class SecurityResult {
        private String suggest;         // Suggestion (pass, review, risky)
        private String label;          // Risk label (if any)
        
        public String getSuggest();
        public void setSuggest(String suggest);
        public String getLabel();
        public void setLabel(String label);
    }
    
    // Getters and setters
    public Integer getErrCode();
    public void setErrCode(Integer errCode);
    public String getErrMsg();
    public void setErrMsg(String errMsg);
    public String getTraceId();
    public void setTraceId(String traceId);
    public SecurityResult getResult();
    public void setResult(SecurityResult result);
    
    // Convenience methods
    public boolean isPassed();
    public boolean isRisky();
    public boolean needsReview();
}

public class WxMaUserRiskRank implements Serializable {
    private Integer riskRank;           // Risk rank (0: normal, 1-4: increasing risk levels)
    private String openid;              // User OpenID
    private Long timestamp;             // Assessment timestamp
    
    // Getters and setters
    public Integer getRiskRank();
    public void setRiskRank(Integer riskRank);
    public String getOpenid();
    public void setOpenid(String openid);
    public Long getTimestamp();
    public void setTimestamp(Long timestamp);
    
    // Convenience methods
    public boolean isHighRisk();
    public boolean isNormalRisk();
    public String getRiskLevel();
}

Media Type Constants

Constants for different media types and security check parameters.

public class WxMaMediaConstants {
    // Media Types
    public static final String IMAGE = "image";
    public static final String VOICE = "voice";  
    public static final String VIDEO = "video";
    public static final String THUMB = "thumb";
    
    // Security Check Scenes
    public static final Integer SCENE_SOCIAL = 1;      // Social scenarios
    public static final Integer SCENE_COMMENT = 2;     // Comment scenarios  
    public static final Integer SCENE_FORUM = 3;       // Forum scenarios
    public static final Integer SCENE_OPEN_API = 4;    // Open API scenarios
    
    // Security Check Versions
    public static final Integer VERSION_LATEST = 2;    // Latest version
    
    // Risk Levels
    public static final Integer RISK_NORMAL = 0;       // Normal user
    public static final Integer RISK_LOW = 1;          // Low risk
    public static final Integer RISK_MEDIUM = 2;       // Medium risk  
    public static final Integer RISK_HIGH = 3;         // High risk
    public static final Integer RISK_VERY_HIGH = 4;    // Very high risk
}

Usage Examples

Media Upload and Management

Upload Images

@Service
public class MediaUploadService {
    
    @Autowired
    private WxMaService wxMaService;
    
    public String uploadUserAvatar(File avatarFile) {
        try {
            // Upload as permanent image (no expiration)
            WxMediaUploadResult result = wxMaService.getMediaService()
                .uploadImg(avatarFile);
            
            String mediaUrl = result.getUrl();
            logger.info("Avatar uploaded successfully: {}", mediaUrl);
            
            return mediaUrl;
            
        } catch (WxErrorException e) {
            logger.error("Avatar upload failed: {}", e.getMessage());
            throw new MediaUploadException("Failed to upload avatar", e);
        }
    }
    
    public String uploadTemporaryImage(MultipartFile file) {
        try {
            // Upload as temporary media (3 days retention)
            WxMediaUploadResult result = wxMaService.getMediaService()
                .uploadTempMedia(WxMaMediaConstants.IMAGE, 
                               file.getInputStream(), 
                               file.getOriginalFilename());
            
            String mediaId = result.getMediaId();
            logger.info("Temporary image uploaded: {}", mediaId);
            
            return mediaId;
            
        } catch (WxErrorException | IOException e) {
            logger.error("Temporary image upload failed: {}", e.getMessage());
            throw new MediaUploadException("Failed to upload temporary image", e);
        }
    }
    
    public String uploadProductImage(File productImage) {
        try {
            // First check image content for security
            boolean isSecure = wxMaService.getSecurityService()
                .checkImage(productImage);
            
            if (!isSecure) {
                throw new SecurityCheckException("Image contains inappropriate content");
            }
            
            // Upload if security check passes
            WxMediaUploadResult result = wxMaService.getMediaService()
                .uploadImg(productImage);
            
            return result.getUrl();
            
        } catch (WxErrorException e) {
            logger.error("Product image upload failed: {}", e.getMessage());
            throw new MediaUploadException("Failed to upload product image", e);
        }
    }
}

Upload Audio and Video

public class MultiMediaService {
    
    public String uploadVoiceMessage(File voiceFile) {
        try {
            // Check audio content first
            boolean isSecure = wxMaService.getSecurityService()
                .checkAudio(voiceFile);
            
            if (!isSecure) {
                throw new SecurityCheckException("Audio contains inappropriate content");
            }
            
            // Upload voice as temporary media
            WxMediaUploadResult result = wxMaService.getMediaService()
                .uploadTempMedia(WxMaMediaConstants.VOICE, voiceFile);
            
            String mediaId = result.getMediaId();
            logger.info("Voice message uploaded: {}", mediaId);
            
            return mediaId;
            
        } catch (WxErrorException e) {
            logger.error("Voice upload failed: {}", e.getMessage());
            throw new MediaUploadException("Failed to upload voice message", e);
        }
    }
    
    public String uploadVideo(File videoFile) {
        try {
            // Upload video as temporary media
            WxMediaUploadResult result = wxMaService.getMediaService()
                .uploadTempMedia(WxMaMediaConstants.VIDEO, videoFile);
            
            String mediaId = result.getMediaId();
            logger.info("Video uploaded: {}", mediaId);
            
            return mediaId;
            
        } catch (WxErrorException e) {
            logger.error("Video upload failed: {}", e.getMessage());
            throw new MediaUploadException("Failed to upload video", e);
        }
    }
    
    public String uploadThumbnail(File thumbnailFile) {
        try {
            // Upload thumbnail for video
            WxMediaUploadResult result = wxMaService.getMediaService()
                .uploadTempMedia(WxMaMediaConstants.THUMB, thumbnailFile);
            
            return result.getMediaId();
            
        } catch (WxErrorException e) {
            logger.error("Thumbnail upload failed: {}", e.getMessage());
            throw new MediaUploadException("Failed to upload thumbnail", e);
        }
    }
}

Media Download and Processing

public class MediaDownloadService {
    
    public File downloadMedia(String mediaId, String targetDirectory) {
        try {
            // Create target file
            File targetFile = new File(targetDirectory, "media_" + mediaId + ".dat");
            
            // Download media
            File downloadedFile = wxMaService.getMediaService()
                .downloadMedia(mediaId, targetFile);
            
            logger.info("Media downloaded: {} -> {}", mediaId, downloadedFile.getPath());
            return downloadedFile;
            
        } catch (WxErrorException e) {
            logger.error("Media download failed for {}: {}", mediaId, e.getMessage());
            throw new MediaDownloadException("Failed to download media", e);
        }
    }
    
    public byte[] downloadMediaAsBytes(String mediaId) {
        try (InputStream inputStream = wxMaService.getMediaService().downloadMedia(mediaId)) {
            return IOUtils.toByteArray(inputStream);
            
        } catch (WxErrorException | IOException e) {
            logger.error("Media download as bytes failed for {}: {}", mediaId, e.getMessage());
            throw new MediaDownloadException("Failed to download media as bytes", e);
        }
    }
    
    @Async
    public CompletableFuture<String> processAndUpload(File originalFile, ImageProcessingOptions options) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // Process image (resize, crop, etc.)
                File processedFile = imageProcessor.process(originalFile, options);
                
                // Upload processed image
                WxMediaUploadResult result = wxMaService.getMediaService()
                    .uploadImg(processedFile);
                
                // Clean up temporary file
                processedFile.delete();
                
                return result.getUrl();
                
            } catch (Exception e) {
                logger.error("Async media processing failed: {}", e.getMessage());
                return null;
            }
        });
    }
}

Content Security and Moderation

Text Content Security Check

@Service
public class ContentModerationService {
    
    public boolean checkTextContent(String content, ContentScene scene) {
        try {
            Integer sceneCode = getSceneCode(scene);
            
            boolean isSecure = wxMaService.getSecurityService()
                .checkText(content, sceneCode, WxMaMediaConstants.VERSION_LATEST);
            
            if (!isSecure) {
                logger.warn("Text content failed security check: {}", 
                           content.substring(0, Math.min(50, content.length())));
            }
            
            return isSecure;
            
        } catch (WxErrorException e) {
            logger.error("Text security check failed: {}", e.getMessage());
            // Default to rejecting on error for safety
            return false;
        }
    }
    
    public ContentModerationResult moderateUserComment(String comment, String openid) {
        try {
            // Check text content
            boolean textSecure = wxMaService.getSecurityService()
                .checkText(comment, WxMaMediaConstants.SCENE_COMMENT, 
                          WxMaMediaConstants.VERSION_LATEST);
            
            // Check user risk level
            WxMaUserRiskRank userRisk = wxMaService.getSecurityService()
                .getUserRiskRank(openid, WxMaMediaConstants.SCENE_COMMENT);
            
            ContentModerationResult result = new ContentModerationResult();
            result.setTextSecure(textSecure);
            result.setUserRiskLevel(userRisk.getRiskRank());
            result.setApproved(textSecure && !userRisk.isHighRisk());
            
            if (result.isApproved()) {
                result.setAction("approve");
            } else if (!textSecure) {
                result.setAction("reject_content");
            } else if (userRisk.isHighRisk()) {
                result.setAction("manual_review");
            }
            
            return result;
            
        } catch (WxErrorException e) {
            logger.error("Comment moderation failed: {}", e.getMessage());
            // Default to manual review on error
            ContentModerationResult result = new ContentModerationResult();
            result.setAction("manual_review");
            result.setApproved(false);
            return result;
        }
    }
    
    private Integer getSceneCode(ContentScene scene) {
        switch (scene) {
            case SOCIAL: return WxMaMediaConstants.SCENE_SOCIAL;
            case COMMENT: return WxMaMediaConstants.SCENE_COMMENT;
            case FORUM: return WxMaMediaConstants.SCENE_FORUM;
            case OPEN_API: return WxMaMediaConstants.SCENE_OPEN_API;
            default: return WxMaMediaConstants.SCENE_SOCIAL;
        }
    }
}

Image Content Security Check

public class ImageModerationService {
    
    public boolean checkUploadedImage(MultipartFile imageFile) {
        try {
            // Check image content
            boolean isSecure = wxMaService.getSecurityService()
                .checkImage(imageFile.getInputStream(), imageFile.getOriginalFilename());
            
            if (!isSecure) {
                logger.warn("Uploaded image failed security check: {}", 
                           imageFile.getOriginalFilename());
            }
            
            return isSecure;
            
        } catch (WxErrorException | IOException e) {
            logger.error("Image security check failed: {}", e.getMessage());
            return false;
        }
    }
    
    public ImageModerationResult moderateUserAvatar(File avatarFile, String openid) {
        try {
            // Check image content
            boolean imageSecure = wxMaService.getSecurityService()
                .checkImage(avatarFile);
            
            // Check user risk level
            WxMaUserRiskRank userRisk = wxMaService.getSecurityService()
                .getUserRiskRank(openid, WxMaMediaConstants.SCENE_SOCIAL);
            
            ImageModerationResult result = new ImageModerationResult();
            result.setImageSecure(imageSecure);
            result.setUserRiskLevel(userRisk.getRiskRank());
            
            // Decision logic
            if (imageSecure && userRisk.getRiskRank() <= WxMaMediaConstants.RISK_LOW) {
                result.setApproved(true);
                result.setAction("approve");
            } else if (!imageSecure) {
                result.setApproved(false);
                result.setAction("reject_inappropriate");
            } else {
                result.setApproved(false);
                result.setAction("manual_review");
            }
            
            return result;
            
        } catch (WxErrorException e) {
            logger.error("Avatar moderation failed: {}", e.getMessage());
            ImageModerationResult result = new ImageModerationResult();
            result.setApproved(false);
            result.setAction("error_occurred");
            return result;
        }
    }
    
    @Async
    public void batchCheckImages(List<String> mediaIds) {
        for (String mediaId : mediaIds) {
            try {
                boolean isSecure = wxMaService.getSecurityService()
                    .checkImage(mediaId);
                
                // Update database with security status
                mediaSecurityRepository.updateSecurityStatus(mediaId, isSecure);
                
                if (!isSecure) {
                    // Handle inappropriate content
                    handleInappropriateMedia(mediaId);
                }
                
                // Rate limiting - small delay between checks
                Thread.sleep(100);
                
            } catch (WxErrorException | InterruptedException e) {
                logger.error("Batch image check failed for {}: {}", mediaId, e.getMessage());
            }
        }
    }
}

User Risk Assessment

@Service
public class UserRiskAssessmentService {
    
    public UserRiskProfile assessUserRisk(String openid, RiskAssessmentContext context) {
        try {
            Integer scene = getSceneFromContext(context);
            
            WxMaUserRiskRank riskResult = wxMaService.getSecurityService()
                .getUserRiskRank(openid, scene);
            
            UserRiskProfile profile = new UserRiskProfile();
            profile.setOpenid(openid);
            profile.setRiskRank(riskResult.getRiskRank());
            profile.setRiskLevel(getRiskLevelDescription(riskResult.getRiskRank()));
            profile.setAssessmentTime(new Date(riskResult.getTimestamp() * 1000));
            profile.setRecommendations(generateRecommendations(riskResult.getRiskRank()));
            
            // Store assessment for future reference
            userRiskRepository.save(profile);
            
            return profile;
            
        } catch (WxErrorException e) {
            logger.error("User risk assessment failed for {}: {}", openid, e.getMessage());
            
            // Return default safe assessment on error
            UserRiskProfile profile = new UserRiskProfile();
            profile.setOpenid(openid);
            profile.setRiskRank(WxMaMediaConstants.RISK_MEDIUM);
            profile.setRiskLevel("Unknown (Assessment Failed)");
            profile.setAssessmentTime(new Date());
            return profile;
        }
    }
    
    public boolean shouldAllowOperation(String openid, UserOperation operation) {
        try {
            Integer scene = getSceneFromOperation(operation);
            WxMaUserRiskRank riskResult = wxMaService.getSecurityService()
                .getUserRiskRank(openid, scene);
            
            // Define operation permissions based on risk level
            switch (operation) {
                case POST_COMMENT:
                    return riskResult.getRiskRank() <= WxMaMediaConstants.RISK_MEDIUM;
                case UPLOAD_IMAGE:
                    return riskResult.getRiskRank() <= WxMaMediaConstants.RISK_HIGH;
                case SEND_MESSAGE:
                    return riskResult.getRiskRank() <= WxMaMediaConstants.RISK_LOW;
                case CREATE_CONTENT:
                    return riskResult.getRiskRank() <= WxMaMediaConstants.RISK_MEDIUM;
                default:
                    return riskResult.getRiskRank() <= WxMaMediaConstants.RISK_LOW;
            }
            
        } catch (WxErrorException e) {
            logger.error("Risk check failed for {} operation {}: {}", 
                       openid, operation, e.getMessage());
            // Default to disallow on error for safety
            return false;
        }
    }
    
    private String getRiskLevelDescription(Integer riskRank) {
        switch (riskRank) {
            case 0: return "Normal";
            case 1: return "Low Risk";
            case 2: return "Medium Risk";
            case 3: return "High Risk";
            case 4: return "Very High Risk";
            default: return "Unknown";
        }
    }
    
    private List<String> generateRecommendations(Integer riskRank) {
        List<String> recommendations = new ArrayList<>();
        
        if (riskRank >= WxMaMediaConstants.RISK_MEDIUM) {
            recommendations.add("Enable manual review for user-generated content");
            recommendations.add("Increase monitoring frequency");
        }
        
        if (riskRank >= WxMaMediaConstants.RISK_HIGH) {
            recommendations.add("Restrict content creation abilities");
            recommendations.add("Require additional verification");
        }
        
        if (riskRank >= WxMaMediaConstants.RISK_VERY_HIGH) {
            recommendations.add("Consider temporary restrictions");
            recommendations.add("Flag for admin review");
        }
        
        return recommendations;
    }
}

Media Processing Pipeline

@Service
public class MediaProcessingPipeline {
    
    @Async
    public CompletableFuture<ProcessedMediaResult> processUploadedMedia(
            MultipartFile file, String openid, MediaProcessingOptions options) {
        
        return CompletableFuture.supplyAsync(() -> {
            try {
                ProcessedMediaResult result = new ProcessedMediaResult();
                
                // Step 1: Basic validation
                if (!validateFile(file)) {
                    result.setSuccess(false);
                    result.setError("Invalid file format or size");
                    return result;
                }
                
                // Step 2: Security check
                boolean isSecure = wxMaService.getSecurityService()
                    .checkImage(file.getInputStream(), file.getOriginalFilename());
                
                if (!isSecure) {
                    result.setSuccess(false);
                    result.setError("Content security check failed");
                    return result;
                }
                
                // Step 3: User risk assessment
                WxMaUserRiskRank userRisk = wxMaService.getSecurityService()
                    .getUserRiskRank(openid, WxMaMediaConstants.SCENE_SOCIAL);
                
                if (userRisk.isHighRisk()) {
                    result.setSuccess(false);
                    result.setError("User risk level too high");
                    return result;
                }
                
                // Step 4: Process image if needed
                File processedFile = file instanceof File ? (File) file : 
                    convertMultipartToFile(file);
                
                if (options.isResizeRequired()) {
                    processedFile = imageProcessor.resize(processedFile, 
                                                        options.getTargetWidth(), 
                                                        options.getTargetHeight());
                }
                
                // Step 5: Upload to WeChat
                WxMediaUploadResult uploadResult;
                if (options.isPermanent()) {
                    uploadResult = wxMaService.getMediaService().uploadImg(processedFile);
                } else {
                    uploadResult = wxMaService.getMediaService()
                        .uploadTempMedia(WxMaMediaConstants.IMAGE, processedFile);
                }
                
                // Step 6: Build result
                result.setSuccess(true);
                result.setMediaId(uploadResult.getMediaId());
                result.setMediaUrl(uploadResult.getUrl());
                result.setProcessingTime(System.currentTimeMillis() - startTime);
                
                // Clean up temporary files
                if (processedFile != file) {
                    processedFile.delete();
                }
                
                return result;
                
            } catch (Exception e) {
                logger.error("Media processing pipeline failed: {}", e.getMessage());
                ProcessedMediaResult result = new ProcessedMediaResult();
                result.setSuccess(false);
                result.setError("Processing pipeline error: " + e.getMessage());
                return result;
            }
        });
    }
    
    private boolean validateFile(MultipartFile file) {
        // Check file size (e.g., max 10MB)
        if (file.getSize() > 10 * 1024 * 1024) {
            return false;
        }
        
        // Check file type
        String contentType = file.getContentType();
        return contentType != null && contentType.startsWith("image/");
    }
}

Content Moderation Dashboard

@RestController
@RequestMapping("/api/moderation")
public class ModerationController {
    
    @PostMapping("/check/text")
    public ResponseEntity<ModerationResult> checkText(
            @RequestBody TextModerationRequest request) {
        
        try {
            boolean isSecure = wxMaService.getSecurityService()
                .checkText(request.getContent(), 
                          request.getScene(), 
                          WxMaMediaConstants.VERSION_LATEST);
            
            ModerationResult result = new ModerationResult();
            result.setSecure(isSecure);
            result.setAction(isSecure ? "approve" : "reject");
            
            return ResponseEntity.ok(result);
            
        } catch (WxErrorException e) {
            logger.error("Text moderation API failed: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ModerationResult(false, "error", e.getMessage()));
        }
    }
    
    @PostMapping("/check/image")  
    public ResponseEntity<ModerationResult> checkImage(
            @RequestParam("file") MultipartFile file) {
        
        try {
            boolean isSecure = wxMaService.getSecurityService()
                .checkImage(file.getInputStream(), file.getOriginalFilename());
            
            ModerationResult result = new ModerationResult();
            result.setSecure(isSecure);
            result.setAction(isSecure ? "approve" : "reject");
            
            return ResponseEntity.ok(result);
            
        } catch (WxErrorException | IOException e) {
            logger.error("Image moderation API failed: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ModerationResult(false, "error", e.getMessage()));
        }
    }
    
    @GetMapping("/user/{openid}/risk")
    public ResponseEntity<UserRiskProfile> getUserRisk(
            @PathVariable String openid,
            @RequestParam(defaultValue = "1") Integer scene) {
        
        try {
            WxMaUserRiskRank riskResult = wxMaService.getSecurityService()
                .getUserRiskRank(openid, scene);
            
            UserRiskProfile profile = new UserRiskProfile();
            profile.setOpenid(openid);
            profile.setRiskRank(riskResult.getRiskRank());
            profile.setRiskLevel(getRiskLevelDescription(riskResult.getRiskRank()));
            profile.setAssessmentTime(new Date(riskResult.getTimestamp() * 1000));
            
            return ResponseEntity.ok(profile);
            
        } catch (WxErrorException e) {
            logger.error("User risk API failed: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

This media and content module provides comprehensive functionality for media file management, content security checks, and user risk assessment with automated moderation capabilities for WeChat MiniApp applications.

Install with Tessl CLI

npx tessl i tessl/maven-com-github-binarywang--weixin-java-miniapp

docs

analytics.md

cloud-development.md

configuration.md

core-services.md

development-tools.md

ecommerce.md

index.md

live-streaming.md

logistics.md

media-content.md

messaging.md

qr-codes.md

user-management.md

tile.json